﻿using P2P.Network.ExtensionMethods;
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Numerics;
using System.Text;

namespace P2P.Network.SocketServer
{
    public delegate void DataReceivedFromCallback(Socket sender, DataEventArgs e);

    public class UDPSocketListener : IDisposable
    {
        #region Fields
        private ConcurrentDictionary<string, Dictionary<int,List<UdpPacket>>> packets = new ConcurrentDictionary<string, Dictionary<int, List<UdpPacket>>>();
        private byte[] receiveBuffer = new byte[4096];
        private IPEndPoint endPoint;
        private Socket listenerSocket;
        private SocketAsyncEventArgs args;

        private DataReceivedFromCallback callback;
        #endregion

        #region Properties

        public IPEndPoint EndPoint
        {
            get { return endPoint; }
            set
            {
                lock (this)
                {
                    if (IsRunning)
                        throw new InvalidOperationException("Property cannot be changed while server running.");
                    else
                        endPoint = value;
                }
            }
        }

        public Boolean IsRunning
        {
            get { return listenerSocket != null; }
        }
        #endregion

        #region Constructors
        public UDPSocketListener(string address, Int32 port, DataReceivedFromCallback callback)
            : this(IPAddress.Parse(address), port, callback)
        {

        }

        public UDPSocketListener(IPAddress address, Int32 port, DataReceivedFromCallback callback)
            : this(new IPEndPoint(address, port), callback)
        {
        
        }

        public UDPSocketListener(IPEndPoint endPoint, DataReceivedFromCallback callback) 
        {
            this.endPoint = endPoint;
            this.callback = callback;
          
            args = new SocketAsyncEventArgs();
            args.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
            args.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
            args.Completed += ReceiveCompleted;
        }

        #endregion

        #region Public Methods
        public void Start()
        {
            lock (this)
            {
                if (!IsRunning)
                {
                    listenerSocket = new Socket(
                        AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
                    listenerSocket.Bind(endPoint);
                    ReceiveData(args);

                    Task.Run(async () => 
                    {
                        while (IsRunning)
                        {
                            CombineBufferData();

                            await Task.Delay(100);
                        }
                    });
                }
                else
                    throw new InvalidOperationException("The Server is already running.");
            }
        }

        public void Stop()
        {
            lock (this)
            {
                if (listenerSocket == null)
                    return;
                listenerSocket.Close();
                listenerSocket = null;
            }
        }

        public int SendTo(Byte[] buffer,IPEndPoint remotePoint) 
        {
            return listenerSocket.SendTo(buffer, remotePoint);
        }

        public int SendTo(Byte[] buffer, IPAddress address, Int32 port)
        {
            return SendTo(buffer,new IPEndPoint(address,port));
        }

        public int SendTo(Byte[] buffer, string address, Int32 port) 
        {
            return SendTo(buffer, new IPEndPoint(IPAddress.Parse(address), port));
        }

        public int SendTo(string buffer, string address, Int32 port)
        {
            return SendTo(Encoding.UTF8.GetBytes(buffer), new IPEndPoint(IPAddress.Parse(address), port));
        }


        #endregion

        #region Private Methods
        private void ReceiveData(SocketAsyncEventArgs args) 
        {
            listenerSocket.InvokeAsyncMethod(new SocketAsyncMethod(listenerSocket.ReceiveFromAsync), ReceiveCompleted, args);
        }

        private void ReceiveCompleted(object sender, SocketAsyncEventArgs e)
        {
            if (e.SocketError == SocketError.Success) 
            {
                int received = e.BytesTransferred;
                if (received > 0)
                {
                    Byte[] data = new Byte[e.BytesTransferred];
                    Array.Copy(e.Buffer, e.Offset, data, 0, data.Length);

                    int identifier = BitConverter.ToInt32(data, 0);
                    int sequenceNumber = BitConverter.ToInt32(data, 4);
                    int totalLength = BitConverter.ToInt32(data, 8);
                    bool isEnableAES = BitConverter.ToBoolean(data, 12);
                    string endPoint = (e.RemoteEndPoint as IPEndPoint).ToString();

                    if (packets.ContainsKey(endPoint))
                    {
                        if (packets[endPoint].ContainsKey(identifier))
                        {
                            UdpPacket packet = new UdpPacket
                            {
                                Identifier = identifier,
                                SequenceNumber = sequenceNumber,
                                TotalLength = totalLength,
                                ReceivedLength = received - 13,
                                IsEnableAES = isEnableAES,
                                Data = new byte[received - 13]
                            };

                            Array.Copy(data, 0, packet.Data, 0, packet.Data.Length);

                            packets[endPoint][identifier].Add(packet);
                        }
                        else 
                        {
                            packets[endPoint] = new Dictionary<int, List<UdpPacket>>();

                            UdpPacket packet = new UdpPacket
                            {
                                Identifier = identifier,
                                SequenceNumber = sequenceNumber,
                                TotalLength = totalLength,
                                ReceivedLength = received - 13,
                                IsEnableAES = isEnableAES,
                                Data = new byte[received - 13]
                            };

                            Array.Copy(data, 0, packet.Data, 0, packet.Data.Length);

                            packets[endPoint][identifier].Add(packet);
                        }
                    }
                    else 
                    {
                        packets[endPoint] = new Dictionary<int, List<UdpPacket>>();

                        UdpPacket packet = new UdpPacket
                        {
                            Identifier = identifier,
                            SequenceNumber = sequenceNumber,
                            TotalLength = totalLength,
                            ReceivedLength = received - 13,
                            IsEnableAES = isEnableAES,
                            Data = new byte[received - 13]
                        };

                        Array.Copy(data, 0, packet.Data, 0, packet.Data.Length);

                        packets[endPoint][identifier].Add(packet);
                    }
                }
            }

            if (!listenerSocket.ReceiveFromAsync(e))
            {
                ReceiveCompleted(listenerSocket, e);
            }
        }

        private void OnDataReceived(Byte[] data,bool isEnableAES, IPEndPoint remoteEndPoint, DataReceivedFromCallback callback)
        {
            callback(this.listenerSocket, new DataEventArgs() { RemoteEndPoint = remoteEndPoint, Data = data, IsEnableAES = isEnableAES });
        }

        private void CombineBufferData()
        {
            Dictionary<string, List<int>> needRemoveIdentifiers = new Dictionary<string, List<int>>();

            var groupedAndSummed = packets.Keys;

            foreach (var entry in groupedAndSummed)
            {
                var values = packets[entry].Values.SelectMany(list => list).GroupBy(p => p.Identifier).ToDictionary(p => p.Key, p => p.Sum(packet => packet.ReceivedLength));

                foreach (var v in values.Keys)
                {
                    var p = packets[entry][v];

                    var firstPacket = p.FirstOrDefault();

                    if (firstPacket != null && firstPacket.TotalLength == values[v])
                    {
                        //包排序
                        p.Sort((p1, p2) => p1.SequenceNumber.CompareTo(p2.SequenceNumber));

                        byte[] data = new byte[firstPacket.TotalLength];

                        int dataLengthAdd = 0;
                        foreach (var packet in p)
                        {
                            Array.Copy(data, dataLengthAdd, packet.Data, 0, packet.Data.Length);
                            dataLengthAdd += packet.Data.Length;
                        }

                        //处理数据包
                        OnDataReceived(data, firstPacket.IsEnableAES, IPEndPoint.Parse(entry), this.callback);

                        if (needRemoveIdentifiers.ContainsKey(entry))
                        {
                            needRemoveIdentifiers[entry].Add(v);
                        }
                        else
                        {
                            needRemoveIdentifiers[entry] = new List<int> { v };
                        }
                    }
                }
            }

            //删除已处理的数据包
            foreach (var endPoint in needRemoveIdentifiers.Keys) 
            {
                foreach (var identifier in needRemoveIdentifiers[endPoint]) 
                {
                     packets[endPoint].Remove(identifier);
                }
            }
        }

        #endregion

        #region IDisposable Members
        private Boolean disposed = false;

        ~UDPSocketListener()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing) 
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    Stop();
                    if (args != null)
                        args.Dispose();
                }

                disposed = true;
            }
        }
        #endregion
    }
}
